﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Dynamic;
using Cjc.Calculation.ExpressionEngine.Excel.Runtime.Binding;
using Cjc.Calculation.ExpressionEngine.Excel.Runtime.Types;
using System.Linq.Expressions;
using Microsoft.Scripting;
using Microsoft.Scripting.Actions;
using Microsoft.Scripting.Ast;
using Microsoft.Scripting.Runtime;

namespace Cjc.Calculation.ExpressionEngine.Excel.Compiler
{
	public partial class ParseTree
	{
        private Dictionary<ExpressionType, ExcelOperationBinder> opBinders = new Dictionary<ExpressionType, ExcelOperationBinder>();

		private ExcelContext context;
		private Dictionary<SymbolId, ParameterExpression> variables = new Dictionary<SymbolId, ParameterExpression>();

		public Expression GetExpression( ExcelContext context )
		{
			this.context = context;

			return GetNodeExpression( this );
		}

		private Expression GetNodeExpression( ParseNode node )
		{
			switch ( node.Token.Type )
			{
				case TokenType.Start:
					return GetStartExpression( node );

				case TokenType.Assignment:
					return GetAssignmentExpression( node );

				case TokenType.CompareExpr:
					return GetComparisonExpression( node );

				case TokenType.AddExpr:
					return GetAddExpression( node );

				case TokenType.MultExpr:
					return GetMultiplyExpression( node );

				case TokenType.Atom:
					return GetAtomExpression( node );

				case TokenType.Range:
					return GetRangeExpression( node );

				case TokenType.TRUE:
					return Expression.Constant( true );

				case TokenType.FALSE:
					return Expression.Constant( false );

				case TokenType.NULL:
					return Expression.Constant( null );

				case TokenType.STRING:
					return GetString( node );

				case TokenType.INTEGER:
					{
						int iVal;
						var value = node.Token.Text;

						return int.TryParse( value, out iVal )
							? Expression.Constant( iVal )
							: Expression.Constant( long.Parse( value ) );
					}

				case TokenType.NUMBER:
					return Expression.Constant( decimal.Parse( node.Token.Text ) );

				case TokenType.Identifier:
					return GetVariableExpression( node );

				case TokenType.Method:
					return GetMethodExpression( node );
			}

			return Expression.Constant( node.Token.Text );
		}

		private IEnumerable<Expression> GetNodeExpressions( ParseNode node )
		{
			var expression = GetNodeExpression( node );

			if ( expression.Type != typeof( Range ) ) return new[] { expression };

			var range = (Range)( (ConstantExpression)expression ).Value;

			return from s in range.GetSymbols() select GetVariableExpression( s );
		}

		private Expression GetStartExpression( ParseNode node )
		{
			return GetNodeExpression( node.Nodes[ 0 ] );
		}

		private Expression GetAssignmentExpression( ParseNode node )
		{
			return Expression.Assign(
				GetVariableExpression( node.Nodes[ 0 ].Token.Text ),
				Utils.Convert( GetNodeExpression( node.Nodes[ 2 ] ), typeof( object ) ) );
		}

		private Expression GetComparisonExpression( ParseNode node )
		{
			ExcelOperationBinder oper = null;
			Expression expr = null;

			foreach ( var n in node.Nodes )
			{
				if ( n.Token.Type == TokenType.COMPARE )
				{
					switch ( n.Token.Text )
					{
						case "=":
						case "==": oper = OpBinder( ExpressionType.Equal ); break;
						case ">": oper = OpBinder( ExpressionType.GreaterThan ); break;
						case ">=": oper = OpBinder( ExpressionType.GreaterThanOrEqual ); break;
						case "<": oper = OpBinder( ExpressionType.LessThan ); break;
						case "<=": oper = OpBinder( ExpressionType.LessThanOrEqual ); break;
						case "!=":
						case "<>": oper = OpBinder( ExpressionType.NotEqual ); break;
					}
				}
				else
				{
					var next = GetNodeExpression( n );

					expr = ( expr == null )
						? next
						: Expression.Dynamic( oper, typeof( object ), expr, next );
				}
			}

			return expr;
		}

		private Expression GetAddExpression( ParseNode node )
		{
			ExcelOperationBinder oper = null;
			Expression expr = null;

			foreach ( var n in node.Nodes )
			{
				if ( n.Token.Type == TokenType.PLUSMINUS )
				{
					switch ( n.Token.Text )
					{
						case "+": oper = OpBinder( ExpressionType.Add ); break;
						case "-": oper = OpBinder( ExpressionType.Subtract ); break;
                        case "&": oper = OpBinder(ExpressionType.Add); break;  // todo Dijji: need operator or function we can restrict to strings
                    }
				}
				else
				{
					var next = GetNodeExpression( n );

					expr = ( expr == null || oper == null )
						? next
						: Expression.Dynamic( oper, typeof( object ), expr, next );
				}
			}

			return expr;
		}

		private Expression GetMultiplyExpression( ParseNode node )
		{
			ExcelOperationBinder oper = null;
			Expression expr = null;

			foreach ( var n in node.Nodes )
			{
				if ( n.Token.Type == TokenType.MULTDIV )
				{
					switch ( n.Token.Text )
					{
						case "*": oper = OpBinder( ExpressionType.Multiply ); break;
						case "/": oper = OpBinder( ExpressionType.Divide ); break;
					}
				}
				else
				{
					var next = GetNodeExpression( n );

					expr = ( expr == null || oper == null )
						? next
						: Expression.Dynamic( oper, typeof( object ), expr, next );
				}
			}

			return expr;
		}

		private Expression GetAtomExpression( ParseNode node )
		{
			foreach ( var n in node.Nodes )
			{
				if ( n.Token.Type != TokenType.BROPEN && n.Token.Type != TokenType.BRCLOSE ) return GetNodeExpression( n );
			}

			return null;
		}

		private Expression GetVariableExpression( ParseNode node )
		{
			return GetVariableExpression( node.Nodes.First().Token.Text );
		}

		private Expression GetVariableExpression( string id )
		{
			return GetVariableExpression( SymbolTable.StringToCaseInsensitiveId( id ) );
		}

		private Expression GetVariableExpression( SymbolId symbol )
		{
			return variables.ContainsKey( symbol )
				? variables[ symbol ]
				: variables[ symbol ] = Expression.Variable( typeof( object ), symbol.ToString() );
		}

		private Expression GetRangeExpression( ParseNode node )
		{
			var cellIds = ( from n in node.Nodes
							where n.Token.Type == TokenType.CELLID
							select new CellId( n.Token.Text ) ).ToArray();

			return ( cellIds.Length > 1 )
				? Expression.Constant( new Range( cellIds ) )
				: GetVariableExpression( cellIds.First().Symbol );
		}

		private Expression GetString( ParseNode node )
		{
			var text = node.Token.Text;

			return Expression.Constant( text.Substring( 1, text.Length - 2 ) );
		}

		private Expression GetMethodExpression( ParseNode node )
		{
			var parameters = ( from n in node.Nodes
							   where n.Token.Type == TokenType.Params
							   from p in n.Nodes
							   where p.Token.Type != TokenType.COMMA
							   from e in GetNodeExpressions( p )
							   select e ).ToArray();

			var name = node.Nodes.First().Token.Text;

			if ( name.ToLower() == "if" ) return GetConditionExpression( parameters );

            // Add a hidden parameter to the method call to pass scope and language
            return Expression.Dynamic(
                context.CreateCallBinder(name, true, new CallInfo(parameters.Length)),
                typeof(object),
                new Expression[] { DlrParameterResolver.ScopeAndLanguageParameter }.Union(parameters).ToArray());
		}

		private Expression GetConditionExpression( Expression[] args )
		{
            var boolBinder = new ExcelConversionBinder(context.Binder as ExcelBinder, typeof(bool), ConversionResultKind.ExplicitCast);
            var objectBinder = new ExcelConversionBinder(context.Binder as ExcelBinder, typeof(object), ConversionResultKind.ExplicitCast);

            // Use dynamic conversions here rather than Expression.Convert, so as to bring all our conversion mechanisms into play
			return Expression.Condition(
                Expression.Dynamic(boolBinder, typeof(bool), args[0]),
                Expression.Dynamic(objectBinder, typeof(object), args[1]),
                Expression.Dynamic(objectBinder, typeof(object), args[2]));
		}

        private ExcelOperationBinder OpBinder(ExpressionType oper)
        {
            return opBinders.ContainsKey(oper)
                ? opBinders[oper]
                : opBinders[oper] = new ExcelOperationBinder(context.Binder as ExcelBinder, oper);
        }
	}
}